using System.Collections.Generic;
using System.Linq;
using System;
using System.Reflection;
using System.Reflection.Emit;

namespace MyUtils.Investigate
{
	class MonoReflectionMethodBodyReader
	{

		static OpCode[] one_byte_opcodes;
		static OpCode[] two_bytes_opcodes;

		static MonoReflectionMethodBodyReader()
		{
			one_byte_opcodes = new OpCode[0xe1];
			two_bytes_opcodes = new OpCode[0x1f];

			var fields = GetOpCodeFields();

			for (int i = 0; i < fields.Length; i++)
			{
				var opcode = (OpCode)fields[i].GetValue(null);
				if (opcode.OpCodeType == OpCodeType.Nternal)
					continue;

				if (opcode.Size == 1)
					one_byte_opcodes[opcode.Value] = opcode;
				else
					two_bytes_opcodes[opcode.Value & 0xff] = opcode;
			}
		}

		static FieldInfo[] GetOpCodeFields()
		{
			return typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static);
		}

		class ByteBuffer
		{

			internal byte[] buffer;
			internal int position;

			public ByteBuffer(byte[] buffer)
			{
				this.buffer = buffer;
			}

			public byte ReadByte()
			{
				CheckCanRead(1);
				return buffer[position++];
			}

			public byte[] ReadBytes(int length)
			{
				CheckCanRead(length);
				var value = new byte[length];
				Buffer.BlockCopy(buffer, position, value, 0, length);
				position += length;
				return value;
			}

			public short ReadInt16()
			{
				CheckCanRead(2);
				short value = (short)(buffer[position]
				                      | (buffer[position + 1] << 8));
				position += 2;
				return value;
			}

			public int ReadInt32()
			{
				CheckCanRead(4);
				int value = buffer[position]
				            | (buffer[position + 1] << 8)
				            | (buffer[position + 2] << 16)
				            | (buffer[position + 3] << 24);
				position += 4;
				return value;
			}

			public long ReadInt64()
			{
				CheckCanRead(8);
				uint low = (uint)(buffer[position]
				                  | (buffer[position + 1] << 8)
				                  | (buffer[position + 2] << 16)
				                  | (buffer[position + 3] << 24));

				uint high = (uint)(buffer[position + 4]
				                   | (buffer[position + 5] << 8)
				                   | (buffer[position + 6] << 16)
				                   | (buffer[position + 7] << 24));

				long value = (((long)high) << 32) | low;
				position += 8;
				return value;
			}

			public float ReadSingle()
			{
				if (!BitConverter.IsLittleEndian)
				{
					var bytes = ReadBytes(4);
					Array.Reverse(bytes);
					return BitConverter.ToSingle(bytes, 0);
				}

				CheckCanRead(4);
				float value = BitConverter.ToSingle(buffer, position);
				position += 4;
				return value;
			}

			public double ReadDouble()
			{
				if (!BitConverter.IsLittleEndian)
				{
					var bytes = ReadBytes(8);
					Array.Reverse(bytes);
					return BitConverter.ToDouble(bytes, 0);
				}

				CheckCanRead(8);
				double value = BitConverter.ToDouble(buffer, position);
				position += 8;
				return value;
			}

			void CheckCanRead(int count)
			{
				if (position + count > buffer.Length)
					throw new ArgumentOutOfRangeException();
			}
		}

		MethodBase method;
		MethodBody body;
		Module module;
		Type[] type_arguments;
		Type[] method_arguments;
		ByteBuffer il;
		ParameterInfo[] parameters;
		IList<LocalVariableInfo> locals;
		List<MonoReflectionInstruction> instructions = new List<MonoReflectionInstruction>();

		MonoReflectionMethodBodyReader(MethodBase method)
		{
			this.method = method;

			this.body = method.GetMethodBody();
			if (this.body == null)
				throw new ArgumentException();

			var bytes = body.GetILAsByteArray();
			if (bytes == null)
				throw new ArgumentException();

			if (!(method is ConstructorInfo))
				method_arguments = method.GetGenericArguments();

			if (method.DeclaringType != null)
				type_arguments = method.DeclaringType.GetGenericArguments();

			this.parameters = method.GetParameters();
			this.locals = body.LocalVariables;
			this.module = method.Module;
			this.il = new ByteBuffer(bytes);
		}

		void ReadInstructions()
		{
			MonoReflectionInstruction previous = null;

			while (il.position < il.buffer.Length)
			{
				var instruction = new MonoReflectionInstruction(il.position, ReadOpCode());

				ReadOperand(instruction);

				if (previous != null)
				{
					instruction.Previous = previous;
					previous.Next = instruction;
				}

				instructions.Add(instruction);
				previous = instruction;
			}
		}

		void ReadOperand(MonoReflectionInstruction instruction)
		{
			switch (instruction.OpCode.OperandType)
			{
				case OperandType.InlineNone:
					break;
				case OperandType.InlineSwitch:
					int length = il.ReadInt32();
					int[] branches = new int[length];
					int[] offsets = new int[length];
					for (int i = 0; i < length; i++)
						offsets[i] = il.ReadInt32();
					for (int i = 0; i < length; i++)
						branches[i] = il.position + offsets[i];

					instruction.Operand = branches;
					break;
				case OperandType.ShortInlineBrTarget:
					instruction.Operand = (sbyte)(il.ReadByte() + il.position);
					break;
				case OperandType.InlineBrTarget:
					instruction.Operand = il.ReadInt32() + il.position;
					break;
				case OperandType.ShortInlineI:
					if (instruction.OpCode == OpCodes.Ldc_I4_S)
						instruction.Operand = (sbyte)il.ReadByte();
					else
						instruction.Operand = il.ReadByte();
					break;
				case OperandType.InlineI:
					instruction.Operand = il.ReadInt32();
					break;
				case OperandType.ShortInlineR:
					instruction.Operand = il.ReadSingle();
					break;
				case OperandType.InlineR:
					instruction.Operand = il.ReadDouble();
					break;
				case OperandType.InlineI8:
					instruction.Operand = il.ReadInt64();
					break;
				case OperandType.InlineSig:
					instruction.Operand = module.ResolveSignature(il.ReadInt32());
					break;
				case OperandType.InlineString:
					instruction.Operand = module.ResolveString(il.ReadInt32());
					break;
				case OperandType.InlineTok:
					instruction.Operand = module.ResolveMember(il.ReadInt32(), type_arguments, method_arguments);
					break;
				case OperandType.InlineType:
					instruction.Operand = module.ResolveType(il.ReadInt32(), type_arguments, method_arguments);
					break;
				case OperandType.InlineMethod:
					instruction.Operand = module.ResolveMethod(il.ReadInt32(), type_arguments, method_arguments);
					break;
				case OperandType.InlineField:
					instruction.Operand = module.ResolveField(il.ReadInt32(), type_arguments, method_arguments);
					break;
				case OperandType.ShortInlineVar:
					instruction.Operand = GetVariable(instruction, il.ReadByte());
					break;
				case OperandType.InlineVar:
					instruction.Operand = GetVariable(instruction, il.ReadInt16());
					break;
				default:
					throw new NotSupportedException();
			}
		}

		object GetVariable(MonoReflectionInstruction instruction, int index)
		{
			if (TargetsLocalVariable(instruction.OpCode))
				return GetLocalVariable(index);
			else
				return GetParameter(index);
		}

		static bool TargetsLocalVariable(OpCode opcode)
		{
			return opcode.Name.Contains("loc");
		}

		LocalVariableInfo GetLocalVariable(int index)
		{
			return locals[index];
		}

		ParameterInfo GetParameter(int index)
		{
			if (!method.IsStatic)
				index--;

			return parameters[index];
		}

		OpCode ReadOpCode()
		{
			byte op = il.ReadByte();
			return op != 0xfe
			       	? one_byte_opcodes[op]
			       	: two_bytes_opcodes[il.ReadByte()];
		}

		public static List<MonoReflectionInstruction> GetInstructions(MethodBase method)
		{
			var reader = new MonoReflectionMethodBodyReader(method);
			reader.ReadInstructions();
			return reader.instructions;
		}
	}
}